package com.ease.framework {

import com.ease.util.Diagnostics;
import com.ease.util.ReflectionUtil;
import com.ease.util.TaggedPropertyInfo;
import com.ease.framework.priv.RequestInfo;
import com.ease.framework.priv.ResponseInfo;

import flash.utils.Dictionary;

import mx.core.Application;
import mx.core.FlexGlobals;

/**
 * @private
 *
 * A registry or manager of the pending request objects known to Frinje, including e.g.,
 * command request events.
 *
 * <p>This registry can be used by other framework classes to keep track of the requests
 * and responses of their interest, as well as check their validity. For example, this
 * registry reports a response as no longer applicable if the context properties (i.e.,
 * properties annotated with the [Context] tag) of the requesting object have changed
 * since making the request and if those context properties are marked to cause such
 * invalidation. Here's an example of how such a property is defined:
 *
 * <listing version="3.0">
 * [Context(changeInvalidatesRequests = "true")]
 * public var someProperty:SomePropertyType;
 * </listing>
 *
 * <p>There can be only one response for a given request object or event at any given
 * time, but there can be multiple responses over the lifetime of the request.
 */
internal class PendingRequestRegistry
{
   private var _infoByRequest:Dictionary = new Dictionary( true );
   private var _infoByResponse:Dictionary = new Dictionary( true );

   //---------------------------------------------------------------------------
   // Instance Methods

   /**
    * Adds the given request object to the set of requests tracked by this registry.
    */
   public final function addRequest( request:Object, requestor:Object ):void {
      if (request == null) {
         return;
      }
      var requestContext:Dictionary = valuesOfRequestInvalidatingProps( requestor );
      var requestInfo:RequestInfo = new RequestInfo( requestor, requestContext );
      _infoByRequest[request] = requestInfo;
   }

   /**
    * Removes all the requests made by a given requestor from this registry.
    */
   public final function removeRequestor( requestor:Object ):void {
      var toRemoveRequest:Array = [];
      var request:Object;
      for (request in _infoByRequest) {
         var requestInfo:RequestInfo = _infoByRequest[request];
         if (requestInfo.requestor == requestor) {
            toRemoveRequest.push(request);
         }
      }
      for each(request in toRemoveRequest) {
         removeRequest(request);
      }

      var toRemoveResponse:Array = [];
      var response:Object;
      for (response in _infoByResponse) {
         var responseInfo:ResponseInfo = _infoByResponse[response];
         if (responseInfo.requestor == requestor) {
            toRemoveResponse.push(response);
         }
      }
      for each(response in toRemoveResponse) {
         removeResponse(response);
      }
   }

   /**
    * Removes the given request object from the set of requests tracked by this registry.
    *
    * <p>Calling this method is not mandatory because once there are no responses for a
    * request (that are known to this registry) and there are no strong references left
    * for that request in the system, the request object will automatically be removed
    * from this registry.
    */
   public final function removeRequest( request:Object ):void {
      var requestInfo:RequestInfo = _infoByRequest[request];
      // Note(mvdb) - It was taking a while for the RequestInfos to go away, keeping
      // other stuff alive, so clear it here since the object will no longer be used.
      requestInfo.clear();
      delete _infoByRequest[request];
   }

   /**
    * Adds the given response object to the set of responses tracked by this registry. The
    * response doesn't need to be associated with a known request.
    *
    * <p>Responses thus added to this registry must be explicitly removed using
    * removeResponse (or else it could lead to unbounded memory growth).
    *
    * <p>Note that Frinje allows a single request to result in multiple responses.
    */
   public final function addResponse( response:Object, responseInfo:ResponseInfo ):void {
      if (response == null || responseInfo == null) {
         return;
      }
      _infoByResponse[response] = responseInfo;
   }

   /**
    * Removes the given response object from the set of responses tracked by this
    * registry.
    *
    * <p>This method must be called corresponding to each addResponse call.
    */
   public final function removeResponse( response:Object ):void {
      var responseInfo:ResponseInfo = _infoByResponse[response];
      // Note(mvdb) - It was taking a while for the ResponseInfos to go away, keeping
      // other stuff alive, so clear it here since the object will no longer be used.
      responseInfo.clear();
      delete _infoByResponse[response];
   }

   /**
    * Returns the request object that is associated with the given response object.
    */
   public final function getResponseInfo( response:Object ):ResponseInfo {
      var responseInfo:ResponseInfo = _infoByResponse[response];
      return responseInfo;
   }

   /**
    * Indicates whether the given object is a response object known to this registry.
    */
   public final function isResponse( object:Object ):Boolean {
      var responseInfo:ResponseInfo = _infoByResponse[object];
      return responseInfo != null;
   }

   /**
    * Returns false only if the given reponse is known to this registry and the response
    * is no longer applicable (e.g., the context of the requesting object, as defined by
    * the [Context] properties, may have changed). Returns true otherwise.
    */
   public final function isResponseStillApplicable( response:Object ):Boolean {
      var responseInfo:ResponseInfo = _infoByResponse[response];
      if (responseInfo == null || responseInfo.request == null) {
         // If the response or the corresponding request are unknown to us, we don't pass
         // a negative judgment.
         return true;
      }
      var requestInfo:RequestInfo = _infoByRequest[responseInfo.request];
      var hasContextChanged:Boolean = hasRequestContextChanged( requestInfo );
      return ! hasContextChanged;
   }

   private static function hasRequestContextChanged( requestInfo:RequestInfo ):Boolean {
      if (requestInfo == null) {
         return false;
      }
      var requestor:Object = requestInfo.requestor;
      if (requestor == null) {
         // There is no requestor object (e.g., the request might have been made by a
         // static method), so there's no applicable context either.
         return false;
      }
      var origPropValByPropName:Dictionary = requestInfo.requestContext;
      for (var propName:String in origPropValByPropName) {
         var oldPropVal:Object = origPropValByPropName[propName];
         var newPropVal:Object = requestor[propName];
         if (oldPropVal != newPropVal) {
            return true;
         }
      }
      return false;
   }

   private static function
   valuesOfRequestInvalidatingProps( requestor:Object ):Dictionary {
      if (requestor == null) {
         // There is no (requestor-related) context if there's no requestor.
         return new Dictionary();
      }
      var props:Array/*TaggedPropertyInfo*/ = ReflectionUtil.getFilteredTaggedProperties(
            requestor, "Context", "changeInvalidatesRequests", "true" );
      var propValByPropName:Dictionary = new Dictionary();
      for each (var pi:TaggedPropertyInfo in props) {
         var propName:String = pi.name;
         var propVal:Object = requestor[propName];
         propValByPropName[propName] = propVal;
      }
      return propValByPropName;
   }
} // class

}